// **********************************************************************
//
// <copyright>
//
//  BBN Technologies
//  10 Moulton Street
//  Cambridge, MA 02138
//  (617) 873-8000
//
//  Copyright (C) BBNT Solutions LLC. All rights reserved.
//
// </copyright>
// **********************************************************************
//
// $Source: /cvs/distapps/openmap/src/openmap/com/bbn/openmap/layer/OMGraphicHandlerLayer.java,v $
// $RCSfile: OMGraphicHandlerLayer.java,v $
// $Revision: 1.33 $
// $Date: 2007/04/24 19:53:44 $
// $Author: dietrick $
//
// **********************************************************************

package com.bbn.openmap.layer;

import java.awt.AlphaComposite;
import java.awt.BasicStroke;
import java.awt.Component;
import java.awt.Composite;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.util.List;
import java.util.Properties;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import com.bbn.openmap.Layer;
import com.bbn.openmap.PropertyConsumer;
import com.bbn.openmap.event.InfoDisplayEvent;
import com.bbn.openmap.event.LayerStatusEvent;
import com.bbn.openmap.event.MapMouseEvent;
import com.bbn.openmap.event.MapMouseListener;
import com.bbn.openmap.event.ProjectionEvent;
import com.bbn.openmap.layer.policy.ProjectionChangePolicy;
import com.bbn.openmap.layer.policy.RenderPolicy;
import com.bbn.openmap.layer.policy.StandardPCPolicy;
import com.bbn.openmap.layer.policy.StandardRenderPolicy;
import com.bbn.openmap.omGraphics.DrawingAttributes;
import com.bbn.openmap.omGraphics.FilterSupport;
import com.bbn.openmap.omGraphics.OMAction;
import com.bbn.openmap.omGraphics.OMGraphic;
import com.bbn.openmap.omGraphics.OMGraphicHandler;
import com.bbn.openmap.omGraphics.OMGraphicList;
import com.bbn.openmap.omGraphics.event.GestureResponsePolicy;
import com.bbn.openmap.omGraphics.event.MapMouseInterpreter;
import com.bbn.openmap.omGraphics.event.StandardMapMouseInterpreter;
import com.bbn.openmap.proj.Projection;
import com.bbn.openmap.tools.icon.IconPart;
import com.bbn.openmap.tools.icon.OMIconFactory;
import com.bbn.openmap.tools.icon.OpenMapAppPartCollection;
import com.bbn.openmap.util.ComponentFactory;
import com.bbn.openmap.util.ISwingWorker;
import com.bbn.openmap.util.PooledSwingWorker;
import com.bbn.openmap.util.PropUtils;

/**
 * The OMGraphicHandlerLayer is a layer that provides OMGraphicHandler support.
 * With this support, the OMGraphicHandlerLayer can accept OMAction instructions
 * for managing OMGraphics, and can perform display filtering as supported by
 * the FilterSupport object.
 * <P>
 *
 * When extending this class for a simple layer, they only method you need to
 * override is the prepare() method. This is a good class to use to start
 * writing your own layers. Start with overriding the prepare() method, having
 * it return an OMGraphicList containing OMGraphics on the map that are
 * appropriate for the current projection.
 * <P>
 *
 * The OMGraphicHandlerLayer already has an OMGraphicList variable, so if you
 * extend this class you don't have to manage another one. You can add your
 * OMGraphics to the list provided with getList(). If you create a list of
 * OMGraphics that is reused and simply re-projected when the projection
 * changes, do nothing - that's what happens anyway based on the default
 * ProjectionChangePolicy set for the layer (StandardPCPolicy). You can either
 * create an OMGraphicList in the constructor and set it by calling
 * setList(OMGraphicList), or you can test for a null OMGraphicList returned
 * from getList() in prepare() and create one if it needs to be. If the list
 * isn't null, make sure you still call generate on it. The advantage of waiting
 * to create the list in prepare is that the processing time to create the
 * OMGraphics is delayed until the layer is added to the map. If you create
 * OMGraphics in the constructor, you delay the entire program (maybe startup of
 * the map!) while the OMGraphics are created.
 * <P>
 *
 * If you let prepare() create a new OMGraphicList based on the new projection,
 * then make sure the ProjectionChangePolicy for the layer is set to a
 * com.bbn.openmap.layer.policy.ResetListPCPolicy, or at least clear out the old
 * graphics at some point before adding new OMGraphics to the list in that
 * method. You just have to do one, not both, of those things. If you are
 * managing a lot of OMGraphics and do not null out the list, you may see your
 * layer appear to lag behind the projection changes. That's because another
 * layer with less work to do finishes and calls repaint, and since your list is
 * still set with OMGraphics ready for the old projection, it will just draw
 * what it had, and then draw again when it has finished working. Nulling out
 * the list will prevent your layer from drawing anything on the new projection
 * until it is ready.
 * <P>
 *
 * The OMGraphicHandlerLayer has support built in for launching a SwingWorker to
 * do work for you in a separate thread. This behavior is controlled by the
 * ProjectionChangePolicy that is set for the layer. Both the StandardPCPolicy
 * and ListResetPCPolicy launch threads by calling doPrepare() on this layer.
 * The StandardPCPolicy only calls this if the number of OMGraphics on its list
 * is greater than some cutoff value.
 * <P>
 *
 * useLayerWorker variable is true (default), then doPrepare() will be called
 * when a new ProjectionEvent is received in the projectionChanged method. This
 * will cause prepare() to be called in a separate thread. You can use prepare()
 * to create OMGraphics, the projection will have been set in the layer and is
 * available via getProjection(). You should generate() the OMGraphics in
 * prepare. NOTE: You can override the projectionChanged() method to
 * create/manage OMGraphics any way you want. The SwingWorker only gets launched
 * if doPrepare() gets called.
 * <P>
 *
 * MouseEvents are not handled by a MapMouseInterpreter, with the layer being
 * the GestureResponsePolicy object dictating how events are responded to. The
 * interpreter does the work of fielding MapMouseEvents, figuring out if they
 * concern an OMGraphic, and asking the policy what it should do in certain
 * situations, including providing tooltips, information, or opportunities to
 * edit OMGraphics. The mouseModes property can be set to the MapMouseMode IDs
 * that the interpreter should respond to.
 * <P>
 *
 * For OMGraphicHandlerLayers, there are several properties that can be set that
 * dictate important behavior:
 *
 * <pre>
 *
 *
 *
 *     layer.projectionChangePolicy=pcp
 *     layer.pcp.class=com.bbn.openmap.layer.policy.StandardPCPolicy
 * 
 *     layer.renderPolicy=srp
 *     layer.srp.class=com.bbn.openmap.layer.policy.StandardRenderPolicy
 *     # or
 *     layer.renderPolicy=ta
 *     layer.ta.class=com.bbn.openmap.layer.policy.RenderingHintsRenderPolicy
 *     layer.ta.renderingHints=KEY_TEXT_ANTIALIASING
 *     layer.ta.KEY_TEXT_ANTIALIASING=VALUE_TEXT_ANTIALIAS_ON
 * 
 *     layer.mouseModes=Gestures
 *     layer.consumeEvents=true
 *
 *
 * </pre>
 */
public class OMGraphicHandlerLayer extends Layer implements GestureResponsePolicy, OMGraphicHandler {

    public static Logger logger = Logger.getLogger("com.bbn.openmap.layer.OMGraphicHandlerLayer");

    /**
     *
     */
    private static final long serialVersionUID = 1L;
    /**
     * The property that can be set for the ProjectionChangePolicy. This
     * property should be set with a scoping marker name used to define a policy
     * class and any other properties that the policy should use.
     * "projectionChangePolicy"
     *
     * @see com.bbn.openmap.layer.policy.ProjectionChangePolicy
     * @see com.bbn.openmap.layer.policy.StandardPCPolicy
     * @see com.bbn.openmap.layer.policy.ListResetPCPolicy
     */
    public final static String ProjectionChangePolicyProperty = "projectionChangePolicy";
    /**
     * The property that can be set for the RenderPolicy. This property should
     * be set with a marker name used to define a policy class and any other
     * properties that the policy should use. "renderPolicy"
     *
     * @see com.bbn.openmap.layer.policy.StandardRenderPolicy
     * @see com.bbn.openmap.layer.policy.BufferedImageRenderPolicy
     * @see com.bbn.openmap.layer.policy.RenderingHintsRenderPolicy
     */
    public final static String RenderPolicyProperty = "renderPolicy";

    /**
     * The property that can be set to tell the layer which mouse modes to
     * listen to. The property should be a space-separated list of mouse mode
     * IDs, which can be specified for a MapMouseMode in the properties file or,
     * if none is specified, the default ID hard-coded into the MapMouseMode.
     * "mouseModes"
     */
    public final static String MouseModesProperty = "mouseModes";

    /**
     * The property that can be set to tell the layer to consume mouse events.
     * The maim reason not to do this is in case you have OMGraphics that you
     * are moving, and you need other layers to respond to let you know when you
     * are over the place you think you need to be.
     */
    public final static String ConsumeEventsProperty = "consumeEvents";

    /**
     * The property to tell the layer how transparent it is. 0 is totally clear,
     * 1f is opaque.
     */
    public final static String TransparencyProperty = "transparency";
    /**
     * The property to tell the layer if the thread launched for prepare()
     * method calls can be interrupted. If false, the thread will be allowed to
     * complete it's work. This (false) is generally a good setting for layers
     * contacting servers. The default setting is, however, true.
     */
    public final static String InterruptableProperty = "interruptable";

    /**
     * Filter support that can be used to manage OMGraphics.
     */
    protected FilterSupport filter = new FilterSupport();

    /**
     * The ProjectionChangePolicy object that determines how a layer reacts and
     * sets up the OMGraphicList to be rendered for the layer when the
     * projection changes.
     */
    protected ProjectionChangePolicy projectionChangePolicy = null;

    /**
     * The RenderPolicy object that determines how a layer's OMGraphicList is
     * rendered in the layer.paint() method.
     */
    protected RenderPolicy renderPolicy = null;

    /**
     * A SwingWorker that can be used for gathering OMGraphics or doing other
     * work in a different thread.
     */
    protected ISwingWorker<OMGraphicList> layerWorker;
    /**
     * A SwingWorker created if the projection changes when the primary
     * layerworker is off doing work. If a bunch of project changes occur before
     * the primary layerworker returns from being interrupted, setting the one
     * in the queue will take care of all of them.
     */
    protected boolean layerWorkerQueue = false;

    protected String[] mouseModeIDs = null;

    /**
     * A flag to tell the layer to be selfish about consuming MouseEvents it
     * receives. If set to true, it will consume events so that other layers
     * will not receive the events. If false, lower layers will also receive
     * events, which will let them react too. Intended to let other layers
     * provide information about what the mouse is over when editing.
     */
    protected boolean consumeEvents = false;

    /**
     * Flag used to avoid the SwingWorker to be interrupted. Useful for layers
     * that load an image from a server such as the WMSPlugin to avoid an ugly
     * java output "Interrupted while loading image".
     */
    protected boolean interruptable = true;

    /**
     * Sets the interruptible flag, allowing the current swing worker thread to
     * have interrupt called on it.
     */
    public void setInterruptable(boolean b) {
        interruptable = b;
    }

    /**
     * Queries for the interruptible flag.
     *
     * @return true if interruptible flag is set, allowing interrupt to be
     *         called on swing worker threads.
     */
    public boolean isInterruptable() {
        return interruptable;
    }

    // OMGraphicHandler methods, deferred to FilterSupport...

    /**
     * Sets all the OMGraphics outside of this shape to be invisible. Also
     * returns another OMGraphicList containing OMGraphics that are contained
     * within the Shape provided.
     */
    public OMGraphicList filter(Shape withinThisShape) {
        return filter.filter(withinThisShape);
    }

    /**
     * @see com.bbn.openmap.omGraphics.OMGraphicHandler#filter(Shape, boolean).
     */
    public OMGraphicList filter(Shape shapeBoundary, boolean getInsideBoundary) {
        return filter.filter(shapeBoundary, getInsideBoundary);
    }

    /**
     * To find out whether SQL queries are handled.
     *
     * @see com.bbn.openmap.omGraphics.OMGraphicHandler#supportsSQL().
     */
    public boolean supportsSQL() {
        return filter.supportsSQL();
    }

    /**
     * Depending on the filter's SQL support, returns an OMGraphicList that fit
     * the query.
     */
    public OMGraphicList filter(String SQLQuery) {
        return filter.filter(SQLQuery);
    }

    /**
     * Perform the OMAction on the OMGraphic, within the OMGraphicList contained
     * in the layer.
     */
    public boolean doAction(OMGraphic graphic, OMAction action) {
        return filter.doAction(graphic, action);
    }

    /**
     * Get the OMGraphicList held by the layer. May be null.
     */
    public OMGraphicList getList() {
        return filter.getList();
    }

    /**
     * Indicates if the OMGraphicHandler can have its OMGraphicList set.
     */
    public boolean canSetList() {
        return filter.canSetList();
    }

    /**
     * Set the OMGraphicList within this OMGraphicHandler. Works if
     * canSetGraphicList == true.
     */
    public void setList(OMGraphicList omgl) {
        filter.setList(omgl);
    }

    /**
     * Remove all filters, and reset all graphics to be visible.
     */
    public void resetFiltering() {
        filter.resetFiltering();
    }

    /**
     * Don't set to null. This is here to let subclasses put a more/less capable
     * FilterSupport in place.
     */
    public void setFilter(FilterSupport fs) {
        filter = fs;
    }

    /**
     * Get the FilterSupport object that is handling the OMGraphicHandler
     * methods.
     */
    public FilterSupport getFilter() {
        return filter;
    }

    /**
     * From the ProjectionListener interface. The method gets called when the
     * layer is part of the map, and whenever the map projection changes. Will
     * trigger a repaint().
     * <p>
     *
     * The ProjectionEvent is passed to the current ProjectionChangePolicy
     * object, which determines what will happen on the layer and how. By
     * default, a StandardPCPolicy is notified with the projection change, and
     * it will test the projection for changes and make sure prepare() is
     * called. It will make the decision whether doPrepare() is called, based on
     * the number of OMGraphics on the list, which may launch a swing worker
     * thread to call prepare(). The StandardPCPolicy does not do anything to
     * the OMGraphicList when the projection changes.
     * <p>
     *
     * If you need the OMGraphicList cleared out with a new projection, you can
     * substitute a ListRestPCPolicy for the StandardPCPolicy. You would want to
     * do this if your OMGraphicList changes for different projections - The
     * reason the OMGraphicList is nulled out is so if another layer finishes
     * before yours does and gets repainted, your old OMGraphics don't get
     * painted along side their new ones - it's a mismatched situation. You can
     * set the ProjectionChangePolicy directly with the
     * setProjectionChangePolicy, or by overriding the getProjectionChangePolicy
     * method and returning the type you want by default if it is null.
     *
     * @see com.bbn.openmap.layer.policy.ProjectionChangePolicy
     * @see com.bbn.openmap.layer.policy.StandardPCPolicy
     * @see com.bbn.openmap.layer.policy.ListResetPCPolicy
     */
    public void projectionChanged(ProjectionEvent pe) {
        if (logger.isLoggable(Level.FINE)) {
            logger.fine("OMGraphicHandlerLayer " + getName() + " projection changed, calling "
                    + getProjectionChangePolicy().getClass().getName());
        }

        getRenderPolicy().prePrepare(pe.getProjection());
        getProjectionChangePolicy().projectionChanged(pe);
    }

    /**
     * Get the ProjectionChangePolicy that determines how a layer reacts and
     * gathers OMGraphics for a projection change.
     */
    public ProjectionChangePolicy getProjectionChangePolicy() {
        if (projectionChangePolicy == null) {
            projectionChangePolicy = new StandardPCPolicy(this);
        }
        return projectionChangePolicy;
    }

    /**
     * Set the ProjectionChangePolicy that determines how a layer reacts and
     * gathers OMGraphics for a projection change.
     */
    public void setProjectionChangePolicy(ProjectionChangePolicy pcp) {
        projectionChangePolicy = pcp;
        // Just to make sure,
        pcp.setLayer(this);
    }

    /**
     * Get the RenderPolicy that determines how an OMGraphicList is rendered.
     */
    public RenderPolicy getRenderPolicy() {
        if (renderPolicy == null) {
            renderPolicy = new StandardRenderPolicy(this);
        }
        return renderPolicy;
    }

    /**
     * Set the RenderPolicy that determines how the OMGraphicList is rendered.
     */
    public void setRenderPolicy(RenderPolicy rp) {
        renderPolicy = rp;
        // Just to make sure,
        rp.setLayer(this);
    }

    /**
     * Sets the SwingWorker off to call prepare(). If the SwingWorker passed in
     * is not null, start() is called on it.
     *
     * @param worker null to reset the layerWorker variable, or a SwingWorker to
     *        start up.
     */
    protected void setLayerWorker(ISwingWorker<OMGraphicList> worker) {
        synchronized (LAYERWORKER_LOCK) {
            layerWorker = worker;
            layerWorkerQueue = false;
        }

        if (worker != null) {
            worker.start();
        }
    }

    protected ISwingWorker<OMGraphicList> getLayerWorker() {
        return layerWorker;
    }

    /**
     * Called from within the layer to create a LayerWorker to use for the
     * prepare() method. By default, a new LayerWorker is returned. This method
     * may be overridden to make the layer use an extended
     * LayerWorker/SwingWorker class.
     *
     * @return SwingWorker/LayerWorker
     */
    protected ISwingWorker<OMGraphicList> createLayerWorker() {
        return new LayerWorker();
    }

    /**
     * Overrides Layer.dispose(), makes sure the OMGraphicList is cleared.
     */
    public void dispose() {
        super.dispose();
        setList(null);
    }

    /**
     * This method is here to provide a default action for Layers as they act as
     * a ProjectionPainter. Normally, ProjectionPainters are expected to receive
     * the projection, gather/create OMGraphics that apply to the projection,
     * and render them into the Graphics provided. This is supposed to be done
     * in the same thread that calls this function, so the caller knows that
     * when this method returns, everything that the ProjectionPainter needed to
     * do is complete.
     * <P>
     * If the layer doesn't override this method, then the paint(Graphics)
     * method will be called.
     *
     * @param proj Projection of the map.
     * @param g java.awt.Graphics to draw into.
     */
    public synchronized void renderDataForProjection(Projection proj, Graphics g) {
        if (proj == null) {
            logger.warning("Layer(" + getName() + ").renderDataForProjection: null projection!");
            return;
        } else if (!proj.equals(getProjection())) {
            setProjection(proj.makeClone());
            setList(getRenderPolicy().prepare());
        }
        paint(g);
    }

    /**
     * The default action is to get the OMGraphicList and render it.
     *
     * @param g java.awt.Graphics object to render OMGraphics into.
     */
    public void paint(Graphics g) {
        getRenderPolicy().paint(g);
    }

    /**
     * A method that will launch a LayerWorker thread to call the prepare
     * method. This method will set in motion all the steps needed to create and
     * render the current OMGraphicList with the current projection. Nothing
     * more needs to be called, because the LayerWorker will be started, it will
     * call prepare(). Inside the prepare() method, the OMGraphicList should be
     * created and the OMGraphics generated for the current projection that can
     * be picked up in the getProjection() method, and the LayerWorker will call
     * workerComplete() which will call repaint() on this layer.
     */
    public void doPrepare() {
        synchronized (LAYERWORKER_LOCK) {

            if (layerWorkerQueue) {
                return;
            }

            ISwingWorker<OMGraphicList> currentLayerWorker = layerWorker;

            if (currentLayerWorker != null) {
                layerWorkerQueue = true;
                if (interruptable) {
                    currentLayerWorker.interrupt();
                }
                return;
            }

            setLayerWorker(createLayerWorker());
        }
    }

    /**
     * A check to see if the LayerWorker (SwingWorker) exists (is doing
     * something).
     */
    public boolean isWorking() {
        // We don't care if it hasn't been interrupted - since the
        // LayerWorker will launch a new thread when things settle out, we
        // just want to know if there is a LayerWorker in place. If multiple
        // doPrepare() calls come in, we need to ignore all of the requests
        // that have come it after the first one that canceled the
        // LayerWorker in the first place.
        synchronized (LAYERWORKER_LOCK) {
            return (layerWorker != null);
        }
    }

    /**
     * This is the main method you should be concerned with when overriding this
     * class. You have to make sure that this method returns an OMGraphicList
     * that is ready to be rendered. That means they need to be generated with
     * the current projection, which can be retrieved by calling
     * getProjection().
     * <P>
     *
     * This method will be called in a separate thread if doPrepare() is called
     * on the layer. This will automatically cause repaint() to be called, which
     * lets java know to call paint() on this class.
     * <P>
     *
     * Note that the default action of this method is to get the OMGraphicList
     * as it is currently set in the layer, re-projects the list with the
     * current projection (calls generate() on them), and then returns the
     * current list.
     * <P>
     *
     * If your layer needs to change what is on the list based on what the
     * current projection is, you can either clear() the list yourself and add
     * new OMGraphics to it (remember to call generate(Projection) on them), and
     * return the list. You also have the option of setting a ListResetPCPolicy,
     * which will automatically set the list to null when the projection changes
     * before calling this method. The OMGraphicHandlerList will ignore a null
     * OMGraphicList.
     * <P>
     *
     * NOTE: If you call prepare directly, you may need to call repaint(), too.
     * With all invocations of this method that are cause by a projection
     * change, repaint() will be called for you.
     *
     * The method is synchronized in case renderDataForProjection() gets called
     * while in the middle of this method. For a different projection, that
     * would be bad.
     */
    public synchronized OMGraphicList prepare() {
        OMGraphicList currentList = getList();
        Projection proj = getProjection();

        // if the layer hasn't been added to the MapBean
        // the projection could be null.
        if (currentList != null && proj != null) {
            currentList.generate(proj);
        }

        return currentList;
    }

    /**
     * Lock object used for managing LayerWorker and queue synchronization.
     */
    protected final Object LAYERWORKER_LOCK = new Object();

    /**
     * Check to see if it's likely the current thread will be replaced with
     * another one.
     * 
     * @return true if another layer worker is queued up.
     */
    public boolean isCancelled() {
        synchronized (LAYERWORKER_LOCK) {
            return layerWorkerQueue;
        }
    }

    /**
     * The LayerWorker calls this method on the layer when it is done working.
     * If the calling worker is not the same as the "current" worker, then a new
     * worker is created.
     *
     * @param worker the worker that has the graphics, must not be null.
     */
    protected void workerComplete(ISwingWorker<OMGraphicList> worker) {

        boolean finishUpWithWorker = false;

        synchronized (LAYERWORKER_LOCK) {
            finishUpWithWorker = !layerWorkerQueue;
            setLayerWorker(layerWorkerQueue ? createLayerWorker() : null);
            layerWorkerQueue = false;
        }

        if (finishUpWithWorker) {
            // CAUTION! layer.repaint() is called in workerComplete!!
            getProjectionChangePolicy().workerComplete(worker.get());
        }
    }

    /**
     * Since we can't have the main thread taking up the time to do the work to
     * create OMGraphics, we use this worker thread to do it.
     */
    class LayerWorker extends PooledSwingWorker<OMGraphicList> {
        /** Constructor used to create a worker thread. */
        public LayerWorker() {
            super();
        }

        /**
         * Compute the value to be returned by the <code>get</code> method.
         */
        public OMGraphicList construct() {
            logger.fine(getName() + "|LayerWorker.construct()");
            fireStatusUpdate(LayerStatusEvent.START_WORKING);
            String errorMsg = null;

            try {
                long start = System.currentTimeMillis();
                OMGraphicList list = getRenderPolicy().prepare();
                long stop = System.currentTimeMillis();
                if (logger.isLoggable(Level.FINE)) {
                    logger.fine(getName() + "|LayerWorker.construct(): fetched "
                            + (list == null ? "null list " : (list.size() + " graphics ")) + "in "
                            + (double) ((stop - start) / 1000d) + " seconds");
                }
                return list;

            } catch (OutOfMemoryError e) {
                errorMsg = getName() + "|LayerWorker.construct(): " + e.getMessage();
                if (logger.isLoggable(Level.FINER)) {
                    logger.fine(errorMsg);
                    e.printStackTrace();
                } else {
                    logger.info(getName() + " layer ran out of memory, attempting to recover...");
                }
            } catch (Throwable e) {
                errorMsg = getName() + "|LayerWorker.construct(): " + e.getClass().getName() + ", "
                        + e.getMessage();
                logger.info(errorMsg);
                e.printStackTrace();
            }

            // This is only called if there is an error.
            if (errorMsg != null && logger.isLoggable(Level.FINE)) {
                fireRequestMessage(new InfoDisplayEvent(this, errorMsg));
            }

            return null;
        }

        /**
         * Called on the event dispatching thread (not on the worker thread)
         * after the <code>construct</code> method has returned.
         */
        public void finished() {
            workerComplete(this);
            if (!isInterrupted()) {
                fireStatusUpdate(LayerStatusEvent.FINISH_WORKING);
            }
        }

        public String toString() {
            return getName() + " LayerWorker";
        }

    }

    /**
     * Overrides the Layer setProperties method. Also calls Layer's version. If
     * the ProjectionChangePolicy and RenderPolicy objects are set
     * programmatically, are PropertyConsumers and the .class property is not
     * set, they will still have access to properties if this method is called.
     * Their property prefix will be scoped as if the OMGraphicHandlerLayer had
     * them created, with their prefix being prefix + . +
     * PropertyChangePolicyProperty and prefix + . + RenderPolicyProperty. If
     * the .class property is set, then a new policy object will be created and
     * replace the one set before this method is called.
     *
     * @param prefix the token to prefix the property names
     * @param props the <code>Properties</code> object
     */
    public void setProperties(String prefix, Properties props) {
        super.setProperties(prefix, props);

        String realPrefix = PropUtils.getScopedPropertyPrefix(prefix);

        // Check to see if the layer wants to set its own projection
        // change policy.
        String pcpString = props.getProperty(realPrefix + ProjectionChangePolicyProperty);
        String policyPrefix;
        if (pcpString != null) {
            policyPrefix = realPrefix + pcpString;

            // If the projection change policy is null, try to create
            // it.
            String pcpClass = props.getProperty(policyPrefix + ".class");
            if (pcpClass != null) {

                Object obj = ComponentFactory.create(pcpClass, policyPrefix, props);
                if (obj != null) {

                    if (logger.isLoggable(Level.FINE)) {
                        logger.fine("Layer " + getName() + " setting ProjectionChangePolicy ["
                                + obj.getClass().getName() + "]");
                    }

                    try {
                        setProjectionChangePolicy((ProjectionChangePolicy) obj);
                    } catch (ClassCastException cce) {
                        logger.warning("Layer "
                                + getName()
                                + " has "
                                + policyPrefix
                                + " property defined in properties for ProjectionChangePolicy, but "
                                + policyPrefix + ".class property (" + pcpClass
                                + ") does not define a valid ProjectionChangePolicy. A "
                                + obj.getClass().getName() + " was created instead.");
                    }

                } else {
                    logger.warning("Layer " + getName() + " has " + policyPrefix
                            + " property defined in properties for PropertyChangePolicy, but "
                            + policyPrefix
                            + ".class property does not define a valid PropertyChangePolicy.");
                }

            } else if (projectionChangePolicy != null) { // ProjectionChangePolicy
                                                         // is not null...
                // If the projection change policy is not null and the
                // policy is a PropertyConsumer, pass the properties
                // to the policy. Note that the property prefix for
                // the policy is prefix +
                // ProjectionChangePolicyProperty

                if (projectionChangePolicy instanceof PropertyConsumer) {
                    ((PropertyConsumer) projectionChangePolicy).setProperties(policyPrefix, props);
                }
            } else {
                logger.warning("Layer " + getName() + " has " + policyPrefix
                        + " property defined in properties for PropertyChangePolicy, but "
                        + policyPrefix + ".class property is undefined.");
            }

        } else if (logger.isLoggable(Level.FINE)) {
            logger.fine("Layer " + getName() + " using default ProjectionChangePolicy ["
                    + getProjectionChangePolicy().getClass().getName() + "]");
        }

        // Check to see if the layer want to set its own rendering
        // policy.
        String rpString = props.getProperty(realPrefix + RenderPolicyProperty);
        if (rpString != null) {
            policyPrefix = realPrefix + rpString;
            String rpClass = props.getProperty(policyPrefix + ".class");

            if (rpClass != null) {

                Object rpObj = ComponentFactory.create(rpClass, policyPrefix, props);

                if (rpObj != null) {
                    if (logger.isLoggable(Level.FINE)) {
                        logger.fine("Layer " + getName() + " setting RenderPolicy ["
                                + rpObj.getClass().getName() + "]");
                    }

                    try {
                        setRenderPolicy((RenderPolicy) rpObj);
                    } catch (ClassCastException cce) {
                        logger.warning("Layer " + getName() + " has " + policyPrefix
                                + " property defined in properties for RenderPolicy, but "
                                + policyPrefix + ".class property (" + rpClass
                                + ") does not define a valid RenderPolicy. A "
                                + rpObj.getClass().getName() + " was created instead.");
                    }
                } else {
                    logger.warning("Layer " + getName() + " has " + policyPrefix
                            + " property defined in properties for RenderPolicy, but "
                            + policyPrefix + ".class property (" + rpClass
                            + ") isn't being created.");
                }

            } else if (renderPolicy != null) { // RenderPolicy is not null...
                // Same thing with renderPolicy as with projection
                // change policy.
                if (renderPolicy instanceof PropertyConsumer) {
                    ((PropertyConsumer) renderPolicy).setProperties(policyPrefix, props);
                }
            } else {
                logger.warning("Layer " + getName() + " has " + policyPrefix
                        + " property defined in properties for RenderPolicy, but " + policyPrefix
                        + ".class property is undefined.");
            }

        } else if (logger.isLoggable(Level.FINE)) {
            logger.fine("Layer " + getName() + " using default RenderPolicy ["
                    + getRenderPolicy().getClass().getName() + "]");
        }

        String mmString = props.getProperty(realPrefix + MouseModesProperty);
        if (mmString != null) {
            Vector<String> mmv = PropUtils.parseSpacedMarkers(mmString);
            if (!mmv.isEmpty()) {
                String[] mm = new String[mmv.size()];
                int i = 0;
                for (String it : mmv) {
                    mm[i++] = it;
                }
                setMouseModeIDsForEvents(mm);
            }
        }

        consumeEvents = PropUtils.booleanFromProperties(props, realPrefix + ConsumeEventsProperty, consumeEvents);

        setTransparency(PropUtils.floatFromProperties(props, realPrefix + TransparencyProperty, getTransparency()));

        setInterruptable(PropUtils.booleanFromProperties(props, realPrefix + InterruptableProperty, isInterruptable()));
    }

    /**
     * Overrides Layer getProperties method., also calls that method on Layer.
     * Sets the properties from the policy objects used by this OMGraphicHandler
     * layer.
     */
    public Properties getProperties(Properties props) {
        props = super.getProperties(props);

        String prefix = PropUtils.getScopedPropertyPrefix(this);
        String policyPrefix = null;

        // //// ProjectionChangePolicy

        ProjectionChangePolicy pcp = getProjectionChangePolicy();
        if (pcp instanceof PropertyConsumer) {
            policyPrefix = ((PropertyConsumer) pcp).getPropertyPrefix();
            ((PropertyConsumer) pcp).getProperties(props);
        }

        if (policyPrefix == null) {
            policyPrefix = prefix + "pcp";
        }

        // Whoops, need to make sure pcp is valid but removing the
        // OMGHL prefix from the front of the policy prefix (if
        // applicable). Same for RenderPolicy

        props.put(prefix + ProjectionChangePolicyProperty, policyPrefix.substring(prefix.length()));
        // This has to come after the above line, or the above
        // property will have a trailing period.
        policyPrefix = PropUtils.getScopedPropertyPrefix(policyPrefix);
        props.put(policyPrefix + "class", pcp.getClass().getName());

        RenderPolicy rp = getRenderPolicy();
        if (rp instanceof PropertyConsumer) {
            policyPrefix = ((PropertyConsumer) rp).getPropertyPrefix();
            ((PropertyConsumer) rp).getProperties(props);
        }

        // /// RenderPolicy

        if (policyPrefix == null) {
            policyPrefix = prefix + "rp";
        }

        props.put(prefix + RenderPolicyProperty, policyPrefix.substring(prefix.length()));
        // This has to come after the above line, or the above
        // property will have a trailing period.
        policyPrefix = PropUtils.getScopedPropertyPrefix(policyPrefix);
        props.put(policyPrefix + "class", rp.getClass().getName());

        props.put(prefix + ConsumeEventsProperty, new Boolean(consumeEvents).toString());

        String[] mm = getMouseModeIDsForEvents();
        if (mm != null && mm.length > 0) {
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < mm.length; i++) {
                // Don't need any MouseModes that have been scoped to
                // the pretty name, those are automatically generated.
                if (mm[i].equals(getName())) {
                    continue;
                }
                sb.append(mm[i]).append(" ");
            }
            props.put(prefix + MouseModesProperty, sb.toString());
        }

        props.put(prefix + TransparencyProperty, Float.toString(getTransparency()));

        props.put(prefix + InterruptableProperty, Boolean.toString(isInterruptable()));

        return props;
    }

    /**
     * Overrides Layer getProperties method., also calls that method on Layer.
     * Sets the properties from the policy objects used by this OMGraphicHandler
     * layer.
     */
    public Properties getPropertyInfo(Properties list) {
        list = super.getPropertyInfo(list);

        String policyPrefix = null;

        ProjectionChangePolicy pcp = getProjectionChangePolicy();
        if (pcp instanceof PropertyConsumer) {
            policyPrefix = ((PropertyConsumer) pcp).getPropertyPrefix();
            if (policyPrefix != null) {
                int index = policyPrefix.indexOf(".");
                if (index != -1) {
                    policyPrefix = policyPrefix.substring(index + 1);
                }

                ((PropertyConsumer) pcp).getPropertyInfo(list);
            }
        }

        if (policyPrefix == null) {
            policyPrefix = "pcp";
        }

        PropUtils.setI18NPropertyInfo(i18n, list, OMGraphicHandlerLayer.class, policyPrefix
                + ".class", "Projection Change Policy", "Class name of ProjectionChangePolicy (optional)", null);

        RenderPolicy rp = getRenderPolicy();
        if (rp instanceof PropertyConsumer) {
            policyPrefix = ((PropertyConsumer) rp).getPropertyPrefix();

            if (policyPrefix != null) {
                int index = policyPrefix.indexOf(".");
                if (index != -1) {
                    policyPrefix = policyPrefix.substring(index + 1);
                }
            }

            ((PropertyConsumer) rp).getPropertyInfo(list);
        } else {
        }

        if (policyPrefix == null) {
            policyPrefix = "rp";
        }

        PropUtils.setI18NPropertyInfo(i18n, list, OMGraphicHandlerLayer.class, policyPrefix
                + ".class", "Rendering Policy", "Class name of RenderPolicy (optional)", null);

        PropUtils.setI18NPropertyInfo(i18n, list, OMGraphicHandlerLayer.class, ConsumeEventsProperty, "Consume mouse events", "Flag that tells the layer to consume mouse events, or let other layers use them as well.", "com.bbn.openmap.util.propertyEditor.OnOffPropertyEditor");

        PropUtils.setI18NPropertyInfo(i18n, list, OMGraphicHandlerLayer.class, MouseModesProperty, "Mouse modes", "Space-separated list of MouseMode IDs to receive events from.", null);

        PropUtils.setI18NPropertyInfo(i18n, list, OMGraphicHandlerLayer.class, TransparencyProperty, "Transparency", "Transparency setting for layer, between 0 (clear) and 1", null);

        PropUtils.setI18NPropertyInfo(i18n, list, OMGraphicHandlerLayer.class, InterruptableProperty, "Interruptable", "Flat to set whether the layer should immediately stop performing current work when the projection changes.", "com.bbn.openmap.util.propertyEditor.YesNoPropertyEditor");

        return list;
    }

    /**
     * The MapMouseInterpreter used to catch the MapMouseEvents and direct them
     * to layer as referencing certain OMGraphics. Manages how the layer
     * responds to mouse events.
     */
    protected MapMouseInterpreter mouseEventInterpreter = null;

    /**
     * Set the interpreter used to field and interpret MouseEvents, thereby
     * calling GestureResponsePolicy methods on this layer.
     */
    public synchronized void setMouseEventInterpreter(MapMouseInterpreter mmi) {

        if (mmi instanceof StandardMapMouseInterpreter) {
            String[] modeList = getMouseModeIDsForEvents();
            ((StandardMapMouseInterpreter) mmi).setMouseModeServiceList(modeList);
            ((StandardMapMouseInterpreter) mmi).setConsumeEvents(getConsumeEvents());
        }

        if (mouseEventInterpreter != null) {
            // Remove handle
            mouseEventInterpreter.setGRP(null);
        }

        mmi.setGRP(this);
        mouseEventInterpreter = mmi;
    }

    /**
     * Get the interpreter used to field and interpret MouseEvents, thereby
     * calling GestureResponsePolicy methods on this layer. This method checks
     * to see if any mouse modes ids have been set via the
     * getMouseModeIDsForEvents() method, and if there were and the interpreter
     * hasn't been set, it will create a StandardMapMouseInterpreter. Otherwise,
     * it returns whatever has been set as the interpreter, which could be null.
     */
    public synchronized MapMouseInterpreter getMouseEventInterpreter() {
        if (getMouseModeIDsForEvents() != null && mouseEventInterpreter == null) {
            setMouseEventInterpreter(new StandardMapMouseInterpreter(this));
        }

        return mouseEventInterpreter;
    }

    /**
     * Query asked from the MouseDelegator for interest in receiving
     * MapMouseEvents. This returns a MapMouseInterpreter that has been told to
     * listen for events from the MapMouseModes in setMouseModeIDsForEvents().
     */
    public MapMouseListener getMapMouseListener() {
        MapMouseListener mml = getMouseEventInterpreter();

        if (mml != null) {
            if (logger.isLoggable(Level.FINE)) {

                String[] modes = mml.getMouseModeServiceList();
                StringBuffer sb = new StringBuffer();
                for (int i = 0; i < modes.length; i++) {
                    sb.append(modes[i]).append(", ");
                }

                logger.fine("Layer " + getName() + " returning " + mml.getClass().getName()
                        + " as map mouse listener that listens to: " + sb.toString());
            }
        }

        return mml;
    }

    /**
     * A flag to tell the layer to be selfish about consuming MouseEvents it
     * receives. If set to true, it will consume events so that other layers
     * will not receive the events. If false, lower layers will also receive
     * events, which will let them react too. Intended to let other layers
     * provide information about what the mouse is over when editing.
     */
    public void setConsumeEvents(boolean consume) {
        consumeEvents = consume;

        if (mouseEventInterpreter instanceof StandardMapMouseInterpreter) {
            ((StandardMapMouseInterpreter) mouseEventInterpreter).setConsumeEvents(getConsumeEvents());
        }
    }

    public boolean getConsumeEvents() {
        return consumeEvents;
    }

    /**
     * This is the important method call that determines what MapMouseModes the
     * interpreter for this layer responds to. The MapMouseInterpreter calls
     * this so it can respond to MouseDelegator queries. You can
     * programmatically call setMouseModeIDsForEvents with the mode IDs to set
     * these values, or set the mouseModes property for this layer set to a
     * space-separated list of mode IDs.
     */
    public String[] getMouseModeIDsForEvents() {
        return mouseModeIDs;
    }

    /**
     * Use this method to set which mouse modes this layer responds to. The
     * array should contain the mouse mode IDs.
     */
    public void setMouseModeIDsForEvents(String[] mm) {

        if (logger.isLoggable(Level.FINE)) {
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < mm.length; i++) {
                sb.append(mm[i]).append(" ");
            }

            logger.fine("For layer " + getName() + ", setting mouse modes to " + sb.toString());
        }

        mouseModeIDs = mm;

        if (mouseEventInterpreter instanceof StandardMapMouseInterpreter) {
            ((StandardMapMouseInterpreter) mouseEventInterpreter).setMouseModeServiceList(mm);
        }
    }

    /**
     * Query asking if OMGraphic is highlight-able, which means that something
     * in the GUI should change when the mouse is moved or dragged over the
     * given OMGraphic. Highlighting shows that something could happen, or
     * provides cursory information about the OMGraphic. Responding true to this
     * method may cause getInfoText() and getToolTipTextFor() methods to be
     * called (depends on the MapMouseInterpetor).
     */
    public boolean isHighlightable(OMGraphic omg) {
        return true;
    }

    /**
     * Query asking if an OMGraphic is select-able, or able to be moved, deleted
     * or otherwise modified. Responding true to this method may cause select()
     * to be called (depends on the MapMouseInterpertor) so the meaning depends
     * on what the layer does in select.
     */
    public boolean isSelectable(OMGraphic omg) {
        return false;
    }

    /**
     * A current list of select OMGraphics.
     */
    protected OMGraphicList selectedList;

    /**
     * Retrieve the list of currently selected OMGraphics.
     */
    public OMGraphicList getSelected() {
        return selectedList;
    }

    // //// Reactions

    /**
     * Fleeting change of appearance for mouse movements over an OMGraphic.
     */
    public void highlight(OMGraphic omg) {
        omg.select();
        omg.generate(getProjection());
        repaint();
    }

    /**
     * Notification to set OMGraphic to normal appearance.
     */
    public void unhighlight(OMGraphic omg) {
        omg.deselect();
        omg.generate(getProjection());
        repaint();
    }

    /**
     * Designate a list of OMGraphics as selected.
     */
    public void select(OMGraphicList list) {
        if (list != null) {
            for (OMGraphic omg : list) {

                if (selectedList == null) {
                    selectedList = new OMGraphicList();
                }

                if (omg instanceof OMGraphicList && !((OMGraphicList) omg).isVague()) {
                    select((OMGraphicList) omg);
                } else {
                    selectedList.add(omg);
                }

            }
        }
    }

    /**
     * Designate a list of OMGraphics as de-selected.
     */
    public void deselect(OMGraphicList list) {
        if (list != null) {
            for (OMGraphic omg : list) {
                if (omg instanceof OMGraphicList && !((OMGraphicList) omg).isVague()) {
                    deselect((OMGraphicList) omg);
                } else if (selectedList != null) {
                    selectedList.remove(omg);
                }
            }
        }
    }

    /**
     * Remove OMGraphics from the layer.
     */
    public OMGraphicList cut(OMGraphicList omgl) {
        OMGraphicList list = getList();
        if (list != null && omgl != null) {
            list.removeAll(omgl);
        }
        return omgl;
    }

	/**
	 * Return a copy of OMGraphics.
	 */
	public OMGraphicList copy(OMGraphicList omgl) {
		return (OMGraphicList) omgl.clone();
	}

    /**
     * Add OMGraphics to the Layer.
     */
    public void paste(OMGraphicList omgl) {
        OMGraphicList list = getList();
        list.addAll(omgl);
    }

    /**
     * If applicable, should return a short, informational string about the
     * OMGraphic to be displayed in the InformationDelegator. Return null if
     * nothing should be displayed.
     */
    public String getInfoText(OMGraphic omg) {
        return null;
    }

    /**
     * If applicable, should return a tool tip for the OMGraphic. Return null if
     * nothing should be shown.
     */
    public String getToolTipTextFor(OMGraphic omg) {
        return null;
    }

    /**
     * Return a JMenu with contents applicable to a pop-up menu for a location
     * over the map. The pop-up doesn't concern any OMGraphics, and should be
     * presented for a click on the map background.
     *
     * @param mme a MapMouseEvent describing the location over where the menu
     *        items should apply, in case different options are appropriate for
     *        different places.
     * @return a JMenu for the map. Return null or empty List if no input
     *         required.
     */
    public List<Component> getItemsForMapMenu(MapMouseEvent mme) {
        return null;
    }

    /**
     * Return a java.util.List containing input for a JMenu with contents
     * applicable to a pop-up menu for a location over an OMGraphic.
     *
     * @return a List containing options for the given OMGraphic. Return null or
     *         empty list if there are no options.
     */
    public List<Component> getItemsForOMGraphicMenu(OMGraphic omg) {
        return null;
    }

    /**
     * A query from the MapMouseInterpreter wondering if the
     * GestureResponsePolicy wants events pertaining to mouse movements over the
     * map that are not over an OMGraphic. If the GestureResponsePolicy responds
     * true, then the mouseOver and leftClick methods will be called on the
     * GestureResponsePolicy by the interpreter. There is no rightClick method
     * that is called, because a right click will always cause a
     * getItemsForMapMenu() method to be called.
     */
    public boolean receivesMapEvents() {
        return false;
    }

    /**
     * A notification that the mouse cursor has been moved over the map, not
     * over any of the OMGraphics on the GestureResponsePolicy. This only gets
     * called if the response to receiveMapEvents is true.
     *
     * @param mme MapMouseEvent describing the location of the mouse.
     * @return true of this information is to be considered consumed and should
     *         not be passed to anybody else.
     */
    public boolean mouseOver(MapMouseEvent mme) {
        return false;
    }

    /**
     * A notification that the mouse has been clicked with the left mouse button
     * on the map, and not on any of the OMGraphics. This only gets called if
     * the response to receiveMapEvents is true. Right clicks on the map are
     * always reported to the getItemsForMapMenu method.
     *
     * @param mme MapMouseEvent describing the location of the mouse.
     * @return true of this information is to be considered consumed and should
     *         not be passed to anybody else.
     */
    public boolean leftClick(MapMouseEvent mme) {
        return false;
    }

	/**
	 * Create a JPanel that has a slider to control the layer transparency. An
	 * action listener that calls layer repaint() when the value changes will be
	 * added to the slider.
	 *
	 * @param label
	 *            the label for the panel around the slider.
	 * @param orientation
	 *            JSlider.HORIZONTAL/JSlider.VERTICAL
	 * @param initialValue
	 *            an initial transparency value between 0-1, 0 being clear.
	 * @return JPanel with controls for transparency setting.
	 */
	public JPanel getTransparencyAdjustmentPanel(String label, int orientation, float initialValue) {
		JPanel opaquePanel = new JPanel();
		// opaquePanel.setBorder(BorderFactory.createEtchedBorder());

		GridBagLayout gridbag = new GridBagLayout();
		GridBagConstraints c = new GridBagConstraints();
		opaquePanel.setLayout(gridbag);

		c.anchor = GridBagConstraints.NORTHWEST;
		JLabel jb = new JLabel(i18n.get(OMGraphicHandlerLayer.class, "layerTransparency", "Layer Transparency"));
		gridbag.setConstraints(jb, c);
		opaquePanel.add(jb);

		JSlider opaqueSlide = new JSlider(orientation, 0/* min */, 255/* max */,
				(int) (255f * initialValue)/* inital */);
		java.util.Hashtable<Integer, JLabel> dict = new java.util.Hashtable<Integer, JLabel>();
		dict.put(new Integer(0), new JLabel(i18n.get(OMGraphicHandlerLayer.class, "clearSliderLabel", "clear")));
		dict.put(new Integer(255), new JLabel(i18n.get(OMGraphicHandlerLayer.class, "opqueSliderLabel", "opaque")));
		opaqueSlide.setLabelTable(dict);
		opaqueSlide.setPaintLabels(true);
		opaqueSlide.setMajorTickSpacing(50);
		opaqueSlide.setPaintTicks(true);
		opaqueSlide.addChangeListener(new ChangeListener() {
			public void stateChanged(ChangeEvent ce) {
				JSlider slider = (JSlider) ce.getSource();
				if (slider.getValueIsAdjusting()) {
					OMGraphicHandlerLayer.this.setTransparency((float) slider.getValue() / 255f);
					repaint();
				}
			}
		});

		c.gridy = 1;
		gridbag.setConstraints(opaqueSlide, c);
		opaquePanel.add(opaqueSlide);

		return opaquePanel;
	}

	public JButton getRedrawButton(String tooltip) {
		DrawingAttributes da = DrawingAttributes.getDefaultClone();
		da.setStroke(new BasicStroke(3));

		IconPart reloadSymbol = OpenMapAppPartCollection.getReloadSymbol();
		reloadSymbol.setRenderingAttributes(da);

		ImageIcon ii = OMIconFactory.getIcon(25, 25, reloadSymbol);
		JButton redraw = new JButton(ii);
		redraw.setActionCommand(RedrawCmd);
		redraw.setToolTipText("Redraw Layer");
		redraw.addActionListener(this);
		return redraw;
	}

	public JButton getSettingsButton(String tooltip) {
		DrawingAttributes da = DrawingAttributes.getDefaultClone();
		da.setStroke(new BasicStroke(3));

		IconPart settingsSymbol = OpenMapAppPartCollection.getSettingsSymbol();
		settingsSymbol.setRenderingAttributes(da);

		ImageIcon ii = OMIconFactory.getIcon(25, 25, settingsSymbol);
		JButton redraw = new JButton(ii);
		redraw.setActionCommand(DisplayPropertiesCmd);
		redraw.setToolTipText(tooltip);
		redraw.addActionListener(this);
		return redraw;
	}

	/**
	 * Get a default settings panel that contains the layer transparency
	 * setting, the settings button and a layer refresh button.
	 * 
	 * @param clss
	 *            The class for i18n translations
	 * @param opaquenessSetting
	 *            The current opaqueness setting, as some fraction of 255. 1 is
	 *            opaque.
	 * @return JPanel with the components all laid out.
	 */
	public JPanel getDefaultSettingsPanel(Class<?> clss, float opaquenessSetting) {
		JPanel panel = new JPanel();
		GridBagLayout gridbag = new GridBagLayout();
		GridBagConstraints c = new GridBagConstraints();
		panel.setLayout(gridbag);

		c.gridx = 0;
		c.gridy = 0;
		c.gridheight = 2;
		c.insets = new Insets(5, 5, 5, 0);

		JPanel transPanel = getTransparencyAdjustmentPanel(
				i18n.get(clss, "layerTransparencyGUILabel", "Layer Transparency"), JSlider.HORIZONTAL,
				opaquenessSetting);
		gridbag.setConstraints(transPanel, c);
		panel.add(transPanel);

		c.gridx = 1;
		c.gridheight = 1;
		c.insets = new Insets(5, 0, 0, 5);
		JButton jb = getSettingsButton(i18n.get(clss, "layerSettingsButtonTooltip", "Change Layer Settings"));
		gridbag.setConstraints(jb, c);
		panel.add(jb);

		c.gridy = 1;
		c.insets = new Insets(0, 0, 5, 5);
		jb = getRedrawButton(i18n.get(clss, "layerRedrawButtonTooltip", "Refresh Layer"));
		gridbag.setConstraints(jb, c);
		panel.add(jb);

		return panel;
	}

	/**
	 * Set the transparency of the layer. This transparency is applied during
	 * rendering.
	 *
	 * @param value
	 *            0f for clear, 1f for opaque.
	 */
	public void setTransparency(float value) {
		AlphaComposite ac = null;
		if (value != 1f) {
			ac = AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, value);
		}
		getRenderPolicy().setComposite(ac);
	}

	/**
	 * Get the transparency value for this layer.
	 *
	 * @return 1 if opaque, 0 for clear.
	 */
	public float getTransparency() {
		float ret = 1f;
		RenderPolicy rp = getRenderPolicy();

		if (rp != null) {
			Composite comp = rp.getComposite();
			if (comp instanceof AlphaComposite) {
				ret = ((AlphaComposite) comp).getAlpha();
			}
		}

		return ret;
	}

	/**
	 * Override of Layer's actionPerformed method, adds the capability that
	 * calls doPrepare() if the layer is visible and it receives a RedrawCmd
	 * command. Also calls Layer.actionPerformed(ActionEvent).
	 */
	public void actionPerformed(ActionEvent e) {
		super.actionPerformed(e);
		String cmd = e.getActionCommand();
		if (cmd == RedrawCmd) {
			if (isVisible()) {
				doPrepare();
			}
		}
	}
}